package com.xingchi.tornado.security.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.xingchi.tornado.basic.Result;
import com.xingchi.tornado.core.code.SecurityCode;
import com.xingchi.tornado.core.filter.TraceFilter;
import com.xingchi.tornado.security.filter.FullRequestContextFilter;
import com.xingchi.tornado.security.filter.UnifiedAuthenticationFilter;
import com.xingchi.tornado.security.handler.DefaultAccessDeniedHandler;
import com.xingchi.tornado.security.handler.DefaultAuthenticationFailureHandler;
import com.xingchi.tornado.security.handler.DefaultAuthenticationSuccessHandler;
import com.xingchi.tornado.security.handler.DefaultLogoutSuccessHandler;
import com.xingchi.tornado.security.handler.ExceptionAuthenticationEntryPoint;
import com.xingchi.tornado.security.handler.LoginPostProcessor;
import com.xingchi.tornado.security.handler.SameMutexSessionControlAuthenticationStrategy;
import com.xingchi.tornado.security.handler.SessionAttributeServiceDecorator;
import com.xingchi.tornado.security.provider.MobileAuthenticationProvider;
import com.xingchi.tornado.security.provider.UsernamePasswordAuthenticationProvider;
import com.xingchi.tornado.security.service.MobileCodeLoginRepository;
import com.xingchi.tornado.security.service.UsernamePasswordLoginRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.security.SpringSessionBackedSessionRegistry;
import org.springframework.session.security.web.authentication.SpringSessionRememberMeServices;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;

/**
 * 安全配置信息
 *
 * @author xiaoya
 * @date 2024/9/27 13:50
 */
@Configuration
@EnableWebSecurity
@EnableConfigurationProperties(SecurityProperties.class)
@ComponentScan(basePackages = {"com.xingchi.tornado"})
public class SecurityConfig<S extends Session> {

    @Resource
    private SecurityProperties properties;
    @Resource
    private ObjectMapper objectMapper;
    @Resource
    private FindByIndexNameSessionRepository<S> sessionRepository;
    @Resource
    private TraceFilter traceFilter;

    @Resource
    private UsernamePasswordLoginRepository usernamePasswordLoginRepository;
    @Autowired(required = false)
    private MobileCodeLoginRepository mobileCodeLoginRepository;

    @Autowired(required = false)
    private List<LoginPostProcessor<Authentication>> loginPostProcessors;
    @Autowired
    private SessionAttributeServiceDecorator sessionAttributeServiceDecorator;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, SessionRegistry sessionRegistry, AuthenticationManager authenticationManager) throws Exception {

        if (properties.isCsrf()) {
            http.csrf();
        } else {
            http.csrf().disable();
        }

        http.sessionManagement();
        http.authorizeRequests().antMatchers(HttpMethod.OPTIONS).permitAll();
        http.authorizeRequests()
                        .antMatchers("/login").permitAll()
                        .antMatchers(properties.getAllowList().toArray(new String[0])).permitAll()
                        .anyRequest().authenticated();

        http.rememberMe()
                .rememberMeServices(rememberMeServices()).and()
                .exceptionHandling()
                .accessDeniedHandler(defaultAccessDeniedHandler())
                .authenticationEntryPoint(authenticationEntryPoint())
                .and().sessionManagement()
                .sessionCreationPolicy(properties.getSessionMode())
                .sessionFixation().none()
                .maximumSessions(properties.getMaximumSessions())
                .expiredSessionStrategy(sessionInformationExpiredStrategy())
                .sessionRegistry(sessionRegistry);

        if (properties.getSameSessionControl().isEnable()) {
            http.sessionManagement()
                    .sessionAuthenticationStrategy(sameMutexSessionControlAuthenticationStrategy(sessionRegistry));
        }

        UnifiedAuthenticationFilter unifiedAuthenticationFilter = new UnifiedAuthenticationFilter();
        unifiedAuthenticationFilter.setAuthenticationManager(authenticationManager);
        http.addFilterBefore(unifiedAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        http.apply(new UnifiedLoginConfigurer<>(unifiedAuthenticationFilter))
                .successHandler(authenticationSuccessHandler())
                .failureHandler(authenticationFailureHandler())
                .loginPostProcessors(loginPostProcessors)
                .and()
                .requestCache().disable()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login")
                .logoutSuccessHandler(logoutSuccessHandler());


        http.addFilterBefore(traceFilter, UnifiedAuthenticationFilter.class);
        http.addFilterAfter(new FullRequestContextFilter(), UnifiedAuthenticationFilter.class);

        return http.build();

    }

    @Bean
    @ConditionalOnMissingBean(SessionInformationExpiredStrategy.class)
    public SessionInformationExpiredStrategy sessionInformationExpiredStrategy() {
        return event -> {
            HttpServletRequest request = event.getRequest();
            HttpServletResponse response = event.getResponse();
            response.setContentType("application/json; charset=utf-8");
            Result<String> fail = Result.fail(SecurityCode.LOGIN_EXPIRED, "登录过期，请稍后重试");
            response.getWriter().print(objectMapper.writeValueAsString(fail));
        };
    }

    @Bean
    public SpringSessionBackedSessionRegistry<S> sessionRegistry() {
        return new SpringSessionBackedSessionRegistry<>(this.sessionRepository);
    }


    @Bean
    public SameMutexSessionControlAuthenticationStrategy sameMutexSessionControlAuthenticationStrategy(SessionRegistry sessionRegistry) {
        return new SameMutexSessionControlAuthenticationStrategy(sessionRegistry, properties, sessionRepository);
    }

    @Bean
    @ConditionalOnMissingBean
    public SpringSessionRememberMeServices rememberMeServices() {
        SpringSessionRememberMeServices springSessionRememberMeServices = new SpringSessionRememberMeServices();
        springSessionRememberMeServices.setValiditySeconds(properties.getRememberMeTime());
        return springSessionRememberMeServices;
    }

    @Bean
    @ConditionalOnMissingBean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    @ConditionalOnMissingBean
    public AuthenticationManager authenticationManager(MobileAuthenticationProvider mobileAuthenticationProvider,
                                                       UsernamePasswordAuthenticationProvider usernamePasswordAuthenticationProvider) {
        List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
        authenticationProviders.add(mobileAuthenticationProvider);
        authenticationProviders.add(usernamePasswordAuthenticationProvider);
        return new ProviderManager(authenticationProviders);
    }

    @Bean
    @ConditionalOnMissingBean
    public MobileAuthenticationProvider mobileAuthenticationProvider() {
        if (mobileCodeLoginRepository == null) {
            throw new NullPointerException("mobileCodeLoginRepository can not be null");
        }
        return new MobileAuthenticationProvider(mobileCodeLoginRepository);
    }

    @Bean
    @ConditionalOnMissingBean
    public UsernamePasswordAuthenticationProvider usernamePasswordAuthenticationProvider(PasswordEncoder passwordEncoder) {
        if (usernamePasswordLoginRepository == null) {
            throw new NullPointerException("usernamePasswordLoginRepository can not be null");
        }
        return new UsernamePasswordAuthenticationProvider(usernamePasswordLoginRepository, passwordEncoder);
    }

    @Bean
    @ConditionalOnMissingBean
    public ExceptionAuthenticationEntryPoint authenticationEntryPoint() {
        return new ExceptionAuthenticationEntryPoint(objectMapper);
    }

    @Bean
    @ConditionalOnMissingBean
    public AuthenticationSuccessHandler authenticationSuccessHandler() {
        return new DefaultAuthenticationSuccessHandler(objectMapper, properties, sessionAttributeServiceDecorator);
    }

    @Bean
    @ConditionalOnMissingBean
    public AuthenticationFailureHandler authenticationFailureHandler() {
        return new DefaultAuthenticationFailureHandler(objectMapper);
    }

    @Bean
    @ConditionalOnMissingBean
    public DefaultLogoutSuccessHandler logoutSuccessHandler() {
        return new DefaultLogoutSuccessHandler(objectMapper);
    }


    @Bean
    @ConditionalOnMissingBean
    public AccessDeniedHandler defaultAccessDeniedHandler() {
        return new DefaultAccessDeniedHandler(objectMapper);
    }
}
